﻿// ----------------------------------------------------------------------------------
// Sample Code from Fernando Machado Píriz's blog: http://fernandomachadopiriz.com
//
// <copyright company="Fernando Machado Píriz" file="ProgramsEnumerator.cs">
// Copyright (c) Fernando Machado Píriz. Distributed under FSF's GPL v2.</copyright>
// ----------------------------------------------------------------------------------
// This code and information are provided "as is" without warranty of any kind,
// either expressed of implied, including but not limited to the implied warranties
// of merchantability and/or fitness for a particular purpose.
// ----------------------------------------------------------------------------------
namespace ClosePrograms
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.IO;
    using System.Linq;
    using System.Runtime.InteropServices;
    using System.Security.Permissions;
    using System.Text;
    using GivingAPresentation.Commons;

    /// <summary>
    /// Enumerates lists of running programs and closes or terminates running programs.
    /// </summary>
    internal sealed class ProgramsEnumerator
    {
        /// <summary>
        /// This instance variable is filled by <code>EnumWindowsProc</code> during enumeration.
        /// </summary>
        private static List<RunningProgram> programs;

        /// <summary>
        /// Returns program's name guessed from given program's module.
        /// </summary>
        /// <param name="moduleFileName">Full path to program's module.</param>
        /// <returns>Program's name.</returns>
        [EnvironmentPermission(SecurityAction.Demand, Unrestricted = true)]
        public static string GetFriendlyName(string moduleFileName)
        {
            FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(moduleFileName);
            if (!String.IsNullOrEmpty(fileVersionInfo.FileDescription))
            {
                return fileVersionInfo.FileDescription;
            }
            else if (!String.IsNullOrEmpty(fileVersionInfo.ProductName))
            {
                return fileVersionInfo.ProductName;
            }
            else
            {
                return Path.GetFileNameWithoutExtension(moduleFileName);
            }
        }

        /// <summary>
        /// Returns a list containing information about currently running programs.
        /// </summary>
        /// <returns>An <code>IList</code> of <code>RunninProgram</code> instances.</returns>
        public static IList<RunningProgram> GetPrograms()
        {
            programs = new List<RunningProgram>();
            if (NativeMethods.EnumWindows(EnumWindowsProc, IntPtr.Zero) == 0)
            {
                int errorCode = Marshal.GetLastWin32Error();
                if (errorCode != NativeMethods.ERROR_SUCCESS)
                {
                    Marshal.ThrowExceptionForHR(errorCode);
                }
            }

            return programs;
        }

        /// <summary>
        /// Tries to close the main window of all instances of the given program, optionally waiting before returning. Returs a value
        /// indicating if instances of the given program were closed or not.
        /// </summary>
        /// <param name="program">The program to close.</param>
        /// <param name="wait">The optional number of milliseconds to wait before returning.</param>
        /// <returns>Returns <code>true</code> if all instances of the given program were closed; returns <code>false</code> if not; returns
        /// <code>null</code> if the given program is not running.</returns>
        [EnvironmentPermission(SecurityAction.Demand, Unrestricted = true)]
        public static bool? CloseProgram(ProgramToClose program, int wait = 0)
        {
            ExceptionHelper.CheckArgumentNotNull(program, "program");

            bool? result = null;
            Process[] processesToClose = Process.GetProcessesByName(Path.GetFileNameWithoutExtension(program.ModuleFileName));
            foreach (Process process in processesToClose)
            {
                process.CloseMainWindow();
                if (wait > 0)
                {
                    process.WaitForExit(wait);
                }

                result = result.GetValueOrDefault(true) & process.HasExited;                
            }

            return result;
        }

        /// <summary>
        /// Kills all instances of the process asociated with the given program.
        /// </summary>
        /// <param name="program">The program to terminate.</param>
        [EnvironmentPermission(SecurityAction.Demand, Unrestricted = true)]
        public static void KillProcess(ProgramToClose program)
        {
            ExceptionHelper.CheckArgumentNotNull(program, "program");

            Process[] processesToClose = Process.GetProcessesByName(Path.GetFileNameWithoutExtension(program.ModuleFileName));
            foreach (Process process in processesToClose)
            {
                process.Kill();
            }
        }

        /// <summary>
        /// Runs the given program.
        /// </summary>
        /// <param name="program">The program to run.</param>
        [EnvironmentPermission(SecurityAction.Demand, Unrestricted = true)]
        public static void OpenProgram(ProgramToClose program)
        {
            Process.Start(program.ModuleFileName);
        }

        /// <summary>
        /// Called by EnumWindows. Fills the list of programs.
        /// </summary>
        /// <param name="handle">The handle of the window being enumerated.</param>
        /// <param name="param">The settingsManager passed to <code>EnumWindowsProc</code>; not used in this application.</param>
        /// <returns>Allways returns 1.</returns>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes",
            Justification = "The try-catch block is used only to prevent conflicts with windows enumeration in Win32 code; there is no need " +
            "to catch or rethrown the exception.")]
        private static int EnumWindowsProc(IntPtr handle, int param)
        {
            try
            {
                if (!IsUserControllableProgram(handle))
                {
                    return 1;
                }

                StringBuilder sb = new StringBuilder(1024);

                if ((NativeMethods.GetWindowText(handle, sb, sb.Capacity) == 0) &&
                    (Marshal.GetLastWin32Error() != NativeMethods.ERROR_SUCCESS))
                {
                    return 1;
                }

                string windowText = sb.ToString();
                if (String.IsNullOrEmpty(windowText))
                {
                    return 1;
                }

                uint processId;
                uint threadId = NativeMethods.GetWindowThreadProcessId(handle, out processId);
                if ((threadId == 0) && (Marshal.GetLastWin32Error() != NativeMethods.ERROR_SUCCESS))
                {
                    return 1;
                }

                Process process = Process.GetProcessById((int)processId);
                if ((process == null) || (process.MainModule == null) || String.IsNullOrEmpty(process.MainModule.FileName))
                {
                    return 1;
                }

                if (programs.FirstOrDefault(item => item.ModuleFileName.Equals(process.MainModule.FileName)) != null)
                {
                    return 1;
                }

                programs.Add(new RunningProgram(process));
            }
            catch
            {
                // Intentionally left blank
            }

            return 1;
        }

        /// <summary>
        /// Returns true if the given window handle corresponds to the top level window of a user controllable program; returns false
        /// otherwise. User controllable programs usually appear in the taskbar or the icon bar. Non user controllable programs are usually
        /// hidden programs or programs running in background.
        /// </summary>
        /// <param name="handle">The handle of the window to test.</param>
        /// <returns>Returns true if the given window handle corresponds to the top level window of a user controllable program; false 
        /// otherwise</returns>
        private static bool IsUserControllableProgram(IntPtr handle)
        {
            IntPtr style = NativeMethods.GetWindowLongPtr(handle, NativeMethods.GWL_EXSTYLE);
            IntPtr parent = NativeMethods.GetParent(handle);

            bool result =
                NativeMethods.IsWindowVisible(handle) & NativeMethods.GetWindow(handle, NativeMethods.GW_OWNER) == IntPtr.Zero &
                parent == IntPtr.Zero | parent.Equals(NativeMethods.GetDesktopWindow());

            if ((style.ToInt64() & NativeMethods.WS_EX_TOOLWINDOW) == NativeMethods.WS_EX_TOOLWINDOW)
            {
                result = false;
            }

            if ((style.ToInt64() & NativeMethods.WS_EX_APPWINDOW) == NativeMethods.WS_EX_APPWINDOW)
            {
                result = true;
            }

            return result;
        }

        /// <summary>
        /// Win32 API imports.
        /// </summary>
        internal static class NativeMethods
        {
            /// <summary>
            /// Win32 API constant.
            /// </summary>
            internal const int GWL_EXSTYLE = -20;

            /// <summary>
            /// Win32 API constant.
            /// </summary>
            internal const int WS_EX_TOOLWINDOW = 0x80;

            /// <summary>
            /// Win32 API constant.
            /// </summary>
            internal const int WS_EX_APPWINDOW = 0x40000;

            /// <summary>
            /// Win32 API constant.
            /// </summary>
            internal const int GW_OWNER = 4;

            /// <summary>
            /// Win32 API constant.
            /// </summary>
            internal const int ERROR_SUCCESS = 0x0;

            /// <summary>
            /// Delegate used to call <code>EnumWindowsProc</code>.
            /// </summary>
            /// <param name="handle">Handle received during window enumeration.</param>
            /// <param name="param">Parameter passed to <code>EnumWindowsProc</code>; not used in this application.</param>
            /// <returns>1 to continue windows enumeration; 0 otherwise.</returns>
            public delegate int EnumWindowsProcDelegate(IntPtr handle, int param);
            [DllImport("user32.dll")]
            internal static extern int EnumWindows(EnumWindowsProcDelegate lpEnumFunc, IntPtr lParam);

            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true, ThrowOnUnmappableChar = true)]
            internal static extern int GetWindowText(IntPtr hWnd, [Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpString, int nMaxCount);

            [SuppressMessage("Microsoft.Portability", "CA1901:PInvokeDeclarationsShouldBePortable",
                MessageId = "return", Justification = "Cannot recall why")]
            [DllImport("user32.dll", EntryPoint = "GetWindowLong", CharSet = CharSet.Auto, SetLastError = true)]
            internal static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex);

            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            internal static extern IntPtr GetParent(IntPtr hWnd);

            [DllImport("user32.dll", CharSet = CharSet.Auto)]
            [return: MarshalAs(UnmanagedType.Bool)]
            internal static extern bool IsWindowVisible(IntPtr hwnd);

            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            internal static extern IntPtr GetWindow(IntPtr hwnd, int wFlag);

            [DllImport("user32.dll", CharSet = CharSet.Auto)]
            internal static extern IntPtr GetDesktopWindow();

            [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
        }
    }
}
